'''
Game of Life With tkinter
Date 2019/4/8
Version 0.4
'''

import numpy as np
import matplotlib.pyplot as plt 
import tkinter as tk 
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

class GameOfLife(object):
    '''
    Define a class for the Game of Life
    '''
    def __init__(self,cells_shape=(20,20),init_state='random',bnd_condition='empty'):
        ''' initialization '''
        # the size of the area
        self.width = cells_shape[0]
        self.height = cells_shape[1]
        # self.extend_width = self.width + 2  
        # self.extend_height = self.width + 2 
        self.init_state  = init_state       # initial state 
        self.bnd_condition = bnd_condition  # boundary condition 
        self.gen_init()                     # generate the inital state and set boundary condition

    def gen_bnd(self):
        '''Generate the boundary'''
        if self.bnd_condition == 'empty':
            # empty boundary condition
            self.cells[0,:] = np.zeros((1,self.width+2))
            self.cells[-1,:] = np.zeros((1,self.width+2))
            self.cells[:,0] = np.zeros((1,self.height+2))
            self.cells[:,-1] = np.zeros((1,self.height+2))
        elif self.bnd_condition == 'period':
            # periodic boundary condition 
            self.cells[0,0] = self.cells[-2,-2]
            self.cells[0,-1] = self.cells[-2,1]
            self.cells[0,1:-1] = self.cells[-2,1:-1]
            self.cells[-1,1:-1] = self.cells[1,1:-1]
            self.cells[-1,0] = self.cells[1,-2]
            self.cells[-1,-1] = self.cells[1,1]
            self.cells[1:-1,0] = self.cells[1:-1,-2]
            self.cells[1:-1,-1] = self.cells[1:-1,1]

    def gen_init(self):
        ''' Initialize the cells and call gen_bnd'''
        self.steps = 0 # recode the steps
        # cells: add two rows and two columns to calculate neighborhood and boundary 
        self.cells = np.zeros((self.width + 2, self.width + 2))
        if self.init_state == 'random':
            self.cells[1:self.width+1,1:self.height+1] = np.random.randint(2,size=(self.width, self.width))
        elif self.init_state == 'Glider':
            glider = np.zeros((3,3))
            glider[0,1]=glider[1,2]=glider[2,0]=glider[2,1]=glider[2,2]=1
            self.cells[3:6,3:6] = glider
        self.gen_bnd() # generate the boundary condition 
        self.draw_cells = 1 - self.cells[1:self.width+1, 1:self.height+1] # the area for darwing

    def update_state(self):
        '''update the state of cells one step'''
        temp_cells = np.zeros((self.width + 2, self.width + 2 )) 
        mask = np.ones((3,3)) # a matrix using to calculate the sum of the neighborhood
        mask[1,1] = 0 # itself is zero
        for i in range(1,self.width+1):
            for j in range(1,self.height+1):
                ngb = self.cells[i-1:i+2,j-1:j+2]
                ngb_value = np.sum(ngb*mask) # calculate the total live cells in the neighborhood
                if ngb_value == 3:
                    temp_cells[i,j] = 1
                elif ngb_value == 2:
                    temp_cells[i,j] = self.cells[i,j]
                else:
                    temp_cells[i,j] = 0
        self.cells = temp_cells 
        self.gen_bnd() # update the boundary
        self.draw_cells = 1 - self.cells[1:self.width+1, 1:self.height+1]
        self.steps += 1

if __name__ == '__main__':
    # main body of the script

    print('hello')
    game1 = GameOfLife()

    root = tk.Tk()
    root.wm_title("Game of life")

    # Frame1 
    Frame1 = tk.Frame(master=root)
    Frame1.pack(side=tk.TOP)
    # choice of initial conditions
    group1 = tk.LabelFrame(master=Frame1,text="Initial condition")
    group1.grid(row=1,column=1)
    init_v = tk.IntVar()
    init_v.set(1)
    INI_CONDITIONS=[('Random',1),('Glider',2)]
    for keys, num in INI_CONDITIONS:
        b = tk.Radiobutton(group1, text = keys, variable = init_v, value = num)
        b.grid(row=0,column=num)
    # choice of boundary conditions
    group2 = tk.LabelFrame(master=Frame1,text="Boundary condition")
    group2.grid(row=1,column=2)
    bnd_v = tk.IntVar()
    bnd_v.set(1)
    BND_CONDITIONS=[('Empty',1),('Period',2)]
    for keys, num in BND_CONDITIONS:
        b = tk.Radiobutton(group2, text = keys, variable = bnd_v, value = num)
        b.grid(row=0,column=num)
    
    def _set(game=game1):
        '''set the initial state and boundary condition'''
        if int(init_v.get()) == 1:
            game.init_state = 'random'
        elif int(init_v.get()) == 2:
            game.init_state = 'Glider'
        if int(bnd_v.get()) == 1:
            game.bnd_condition = 'empty'
        elif int(bnd_v.get()) == 2:
            game.bnd_condition = 'period'
        game.gen_init()
        ax1.set_title('Initial State')  
        ax1.pcolor(game.draw_cells,cmap='hot',edgecolor='k')
        canvas.draw()
    # set button
    set_button1 = tk.Button(master=Frame1, text="Set", command=_set) 
    set_button1.grid(row=1,column=3)

    # Frame 2: Drawing Area
    fig = plt.figure()  # figure for the drawing
    ax1 = fig.add_subplot(111)
    ax1.set_aspect(1.0)
    ax1.set_xlim([0,game1.width])
    ax1.set_ylim([game1.height,0])  # reverse the y-axis

    canvas = FigureCanvasTkAgg(fig, master=root)   # pass figure to tk Drawing Area 
    canvas.get_tk_widget().pack(side=tk.TOP,fill=tk.BOTH, expand=1)

    # sign for the running the drawing
    running = False 
    
    def _convas_draw(game=game1):
        ''' Drawing and call back '''
        global running
        if running:
            #print(game.steps)
            #ax1.clear()
            ax1.set_title('Steps: {}'.format(game.steps))
            ax1.pcolor(game.draw_cells,cmap='hot',edgecolor='k')
            canvas.draw()
            game.update_state()
        root.after(100, _convas_draw) # After 0.1 second, call _convas_draw, the kew point

    def _start():
        ''' start drawing '''
        global running
        running = True

    def _stop():
        ''' stop drawing '''
        global running
        running = False

    def _quit():
        ''' Quit the script'''
        root.quit()     # stops mainloop
        root.destroy()  # this is necessary on Windows to prevent Fatal Python Error: PyEval_RestoreThread: NULL tstate
    
    # Frame 3
    Frame3 = tk.Frame(master=root)
    Frame3.pack(side=tk.TOP)
    button1 = tk.Button(master=Frame3, text="Start", command=_start) 
    button2 = tk.Button(master=Frame3, text="Stop", command=_stop)     
    button3 = tk.Button(master=Frame3, text="Quit", command=_quit)

    button1.grid(row=1,column=1)
    button2.grid(row=1,column=2)
    button3.grid(row=1,column=3)

    root.after(1000, _convas_draw)
    tk.mainloop()